Skip to content

feat(phaser): add reusable phase coordination - #250

Merged
tisonkun merged 13 commits into
apache:mainfrom
ButterBright:main
Sep 11, 2026
Merged

feat(phaser): add reusable phase coordination#250
tisonkun merged 13 commits into
apache:mainfrom
ButterBright:main

Conversation

@ButterBright

@ButterBright ButterBright commented Aug 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Add an opt-in, runtime-agnostic Phaser for repeated rounds with dynamic, owned participants. register_one() registers one participant; register(n) atomically registers a batch and returns an owning iterator.
  • Support participant wait(), split arrive() / wait(), independent Phaser::wait(observed) observers, and explicit close() with an opaque Closed error.
  • Add practical examples for shared dictionary construction, dynamic workers, coordinator processing between rounds, and grouped coordination with failure propagation.
  • Cover public behavior in integration tests, retain counter-wrap tests internally, and add Phaser benchmarks and Miri coverage. Validated with 521 tests on the current toolchain and Rust 1.86, lint/docs, the feature matrix, workspace builds, and all three runnable examples. Miri passes the supported tests; OS-backed Tokio tests are skipped there.

Design Notes

A participant owns one arrival obligation per phase. Repeated arrivals in that phase count once. Cancelling a polled participant wait preserves its committed arrival and pending phase, so retrying observes the same round. Dropping a participant or the unconsumed portion of a batch withdraws those registrations; withdrawal does not certify successful application work. Applications can close the phaser on failure.

Empty phasers remain dormant and reusable. Closing freezes the unfinished phase and releases its waiters with Closed; already completed phases remain successful. Phase numbers are wrapping u64 observations: waiting detects a change, not arrival at a future numeric target.

Waker registration uses the existing borrowed-waker API inside one state critical section, following the contract adopted in #257. Repeated polls reuse a matching registration; replacement cleanup and notification run outside the lock. The examples express grouped coordination and asynchronous work between rounds by composing phasers and coordinator tasks.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Phaser::poll_wait currently clones wakers unconditionally instead of following the repo’s established WaitSet::will_wake pattern, adding avoidable per-poll overhead.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a new opt-in asyncband::phaser synchronization primitive for coordinating repeated phases with a dynamic participant set, including RAII participants, cancel-safe waiting semantics, and documentation/tests to integrate it into the crate’s public surface.

Changes:

  • Introduce asyncband::phaser::{Phase, Phaser, PhaserParticipant} behind a new phaser feature flag.
  • Add unit + integration tests covering registration/arrival/advance semantics and cross-task waiting.
  • Update crate docs (README + crate-level docs) and changelog to advertise the new primitive.
File summaries
File Description
tests-integration/tests/traits_test.rs Extends trait assertions (Send/Sync/Unpin) to cover new public phaser types.
tests-integration/tests/phaser_test.rs Adds integration tests for spawned-task waiting and observer semantics.
tests-integration/Cargo.toml Enables asyncband’s new phaser feature for integration tests.
README.md Documents the new Phaser feature in the public feature table.
CHANGELOG.md Records the addition of the new opt-in Phaser.
Cargo.lock Captures new dev/test dependency resolution (e.g., tokio-test).
asyncband/src/phaser/tests.rs Adds focused unit tests for phase advancement, cancellation, waker behavior, and wraparound.
asyncband/src/phaser/mod.rs Implements the new Phaser primitive, participants, waiting, and internal state transitions.
asyncband/src/lib.rs Wires the phaser module into the crate behind a feature flag and updates crate docs.
asyncband/src/internal/mod.rs Extends internal module feature gating to include phaser where needed.
asyncband/Cargo.toml Adds the phaser feature and includes tokio-test for module tests.
Review details
  • Files reviewed: 10/11 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread asyncband/src/phaser/mod.rs Outdated
Comment on lines +371 to +388
fn poll_wait(
&self,
token: &mut Option<WakerToken>,
observed: Phase,
cx: &mut Context<'_>,
) -> Poll<Phase> {
let waker = cx.waker().clone();
let _retired_waker = {
let mut state = self.state.lock();
if state.phase != observed {
let phase = state.phase;
*token = None;
return Poll::Ready(phase);
}
state.waiters.register(token, waker)
};
Poll::Pending
}
@tisonkun tisonkun mentioned this pull request Sep 4, 2026
33 tasks
orthur2
orthur2 previously requested changes Sep 4, 2026

@orthur2 orthur2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rebase this onto current main branch first? Since the branch was last updated, #273 replaced WaitSet with WakerSet, and the recent API-table changes now conflict with this branch.

I'd also recommend adding a short Summary to the PR description, in line with AGENTS.md.You can refer to the body of PRs that have already been merged.

I'd be happy to do a more thorough review once it's rebased.

@ButterBright

Copy link
Copy Markdown
Member Author

Updated.

@orthur2 orthur2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update!
I went through the implementation against #220, and it looks good to me. I left a couple of comments on the synchronization guarantee and using the API from a spawned task.

Comment thread CHANGELOG.md Outdated
Comment thread asyncband/src/phaser/mod.rs Outdated
//!
//! Waiters compare phase identity instead of inferring transitions from party counts.
//!
//! # Examples

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add an async example showing a few phases, including separate arrival and waiting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, a spawned task needs a separate Arc<Phaser> to call wait_for_advance() after participant.arrive(). The participant already holds that same Arc internally. I'd suggest exposing a phaser() accessor so callers can use the two operations separately without carrying another handle.

Comment thread asyncband/src/phaser/mod.rs Outdated
//!
//! [`Phaser::arrived_parties`] is the difference between the registered and unarrived counts.
//!
//! All state transitions and waiter registration share one synchronization point.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we spell out the memory visibility guarantee here?

The shared synchronization point explains how the internal transitions are ordered, but does not explicitly tell callers whether operations before each participant's arrival happen-before operations after a wait observes that phase's completion. The shared mutex already provides this guarantee, but it should be explicit in the public API docs.

I added a # Synchronization section for this in ManualResetEvent last week. Something similar would help here.

@ButterBright

Copy link
Copy Markdown
Member Author

Thanks for the review. I've addressed the comments above.

Signed-off-by: tison <wander4096@gmail.com>
Signed-off-by: tison <wander4096@gmail.com>
Signed-off-by: tison <wander4096@gmail.com>
Register under the existing state lock and defer replaced waker destruction until after unlocking, following the contract from apache#257. Remove the separate probe and owned registration APIs, and replace clone-reentrancy coverage with replacement and cancellation drop-reentrancy tests.

@tisonkun tisonkun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution! I reuse your codebase and add some bench baseline and modify the API a bit.

Welcome to drop a review after merge and if anything can be improved, feel free to submit a new PR.

@tisonkun
tisonkun merged commit e77cbef into apache:main Sep 11, 2026
9 checks passed
@ButterBright

Copy link
Copy Markdown
Member Author

Thanks for the refinements. The changes look great. I’ll follow up if I find anything that can be improved.

@tisonkun

Copy link
Copy Markdown
Member

@ButterBright Improvements can be driven by examples -

I see Java Phaser can have parent but the callback style is not like Rust idiom.

You may take a look at other primitives or see if there is common performance tricks (esp. for channels)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants